/*
 * Copyright 2014 Ranjan Kumar
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.restfeel.controller.rest;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.restfeel.constant.NodeType;
import com.restfeel.dao.ConversationRepository;
import com.restfeel.dao.GenericEntityRepository;
import com.restfeel.dao.NodeRepository;
import com.restfeel.dao.ProjectRepository;
import com.restfeel.dao.TagRepository;
import com.restfeel.dao.util.TreeNodeBuilder;
import com.restfeel.dto.NodeDTO;
import com.restfeel.dto.TagDTO;
import com.restfeel.entity.BaseNode;
import com.restfeel.entity.Conversation;
import com.restfeel.entity.GenericEntity;
import com.restfeel.entity.Project;
import com.restfeel.entity.Tag;
import com.restfeel.util.EntityToDTO;
import com.restfeel.util.TreeNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAutoConfiguration
@ComponentScan
@Transactional(propagation = Propagation.REQUIRES_NEW)
public class NodeController {
    private static final String PROJECT = "PROJECT";

    Logger logger = LoggerFactory.getLogger(NodeController.class);

    @Autowired
    private ProjectRepository projectRepository;

    @Autowired
    private NodeRepository nodeRepository;

    @Autowired
    private TagRepository tagRepository;

    @Autowired
    private ConversationRepository conversationRepository;

    @Autowired
    private GenericEntityRepository genericEntityRepository;

    @Autowired
    private GenerateApiController generateApiController;

    // Note : Creating a node requires parentId. Project-node is the root node and it is created during project
    // creation.
    @RequestMapping(value = "/api/nodes/{parentId}/children", method = RequestMethod.POST,
        headers = "Accept=application/json")
    public
    @ResponseBody
    NodeDTO create(@PathVariable("parentId") String parentId, @RequestBody NodeDTO nodeDTO) {
        logger.debug("Creating a new node with information: " + nodeDTO);

        BaseNode node = new BaseNode();

        node.setName(nodeDTO.getName());
        node.setDescription(nodeDTO.getDescription());
        node.setNodeType(nodeDTO.getNodeType());
        node.setStarred(nodeDTO.getStarred());
        BaseNode parentNode = nodeRepository.findOne(parentId);
        node.setWorkspaceId(parentNode.getWorkspaceId());

        node.setParentId(parentId);

        // To find the last child node and its position
        // Kotlin 调用静态Companion方法
        long lastChildPosition = com.restfeel.util.NodeUtil.Companion.findLastChildPosition(
            nodeRepository.getChildren(parentId));

        // Set the appropriate node position.
        node.setPosition(lastChildPosition + 1);

        node = nodeRepository.save(node);

        if (nodeDTO.getConversationDTO() != null && nodeDTO.getConversationDTO().getId() != null) {
            Conversation conversation = conversationRepository.findOne(nodeDTO.getConversationDTO().getId());
            node.setConversation(conversation);
            conversation.setNodeId(node.getId());
            conversationRepository.save(conversation);
        }

        if (nodeDTO.getGenericEntityDTO() != null && nodeDTO.getGenericEntityDTO().getId() != null) {
            GenericEntity genericEntity = genericEntityRepository.findOne(nodeDTO.getGenericEntityDTO().getId());
            genericEntity.setBaseNodeId(node.getId());
            genericEntityRepository.save(genericEntity);
            node.setGenericEntity(genericEntity);
        }

        Project project = projectRepository.findOne(nodeDTO.getProjectId());
        node.setProjectId(project.getId());

        BaseNode savedNode = nodeRepository.save(node);

        // set tags
        List<Tag> tags = new ArrayList<Tag>();

        List<TagDTO> tagDTOs = nodeDTO.getTags();
        if (tagDTOs != null && !tagDTOs.isEmpty()) {
            List<String> tagIds = new ArrayList<String>();
            for (TagDTO tagDTO : tagDTOs) {
                tagIds.add(tagDTO.getId());
            }
            tags = (List<Tag>)tagRepository.findAll(tagIds);
        }
        savedNode.setTags(tags);
        savedNode = nodeRepository.save(savedNode);

        // Generate APIs for Entity
        if (nodeDTO.getGenericEntityDTO() != null && nodeDTO.getGenericEntityDTO().getId() != null) {
            generateApiController.generateApi(savedNode);
        }
        return EntityToDTO.toDTO(savedNode);
    }

    @RequestMapping(value = "/api/nodes/{id}", method = RequestMethod.DELETE, headers = "Accept=application/json")
    public
    @ResponseBody
    void delete(@PathVariable("id") String id) {
        logger.debug("Deleting node with id: " + id);

        BaseNode nodeToDelete = nodeRepository.findOne(id);
        Long deletedNodePosition = nodeToDelete.getPosition();

        deleteNodesRecursively(nodeToDelete);

        BaseNode parent = nodeRepository.findOne(nodeToDelete.getParentId());
        List<BaseNode> children = nodeRepository.getChildren(parent.getId());
        if (children != null && !children.isEmpty()) {
            for (BaseNode baseNode : children) {
                if (baseNode.getPosition() > deletedNodePosition) {
                    baseNode.setPosition(baseNode.getPosition() - 1);
                    nodeRepository.save(baseNode);
                }
            }
        }
    }

    @RequestMapping(value = "/api/nodes/{id}/copy", method = RequestMethod.POST, headers = "Accept=application/json")
    public
    @ResponseBody
    void copy(@PathVariable("id") String id, @RequestBody NodeDTO nodeDTO) {
        BaseNode node = nodeRepository.findOne(id);
        node.setName(nodeDTO.getName());
        node.setDescription(nodeDTO.getDescription());

        copyNodesRecursively(node, node.getParentId());
    }

    public void copyNodesRecursively(BaseNode node, String parentId) {
        NodeDTO dto = EntityToDTO.toDTO(node);
        NodeDTO newNode = create(parentId, dto);

        String nodeType = node.getNodeType();
        if (nodeType != null
            && (NodeType.FOLDER.name().equalsIgnoreCase(nodeType) || NodeType.PROJECT.name().equalsIgnoreCase(nodeType)
            || NodeType.ENTITY.name()
            .equalsIgnoreCase(nodeType))) {
            List<BaseNode> children = getChildren(node.getId());
            if (children != null && !children.isEmpty()) {
                for (BaseNode childNode : children) {
                    copyNodesRecursively(childNode, newNode.getId());
                }
            }
        }
        // This is just a workaround added for now.
        if (nodeType != null && NodeType.FOLDER.name().equalsIgnoreCase(nodeType)) {
            if (node.getGenericEntity() != null) {
                // TODO : genericEntityRepository.delete(node.getGenericEntity());
            }

        } else if (nodeType != null && NodeType.ENTITY.name().equalsIgnoreCase(nodeType)) {
            // TODO : genericEntityRepository.delete(node.getGenericEntity());
        }

    }

    public void deleteNodesRecursively(BaseNode node) {
        String nodeType = node.getNodeType();
        if (nodeType != null
            && (NodeType.FOLDER.name().equalsIgnoreCase(nodeType) || NodeType.PROJECT.name().equalsIgnoreCase(nodeType)
            || NodeType.ENTITY.name()
            .equalsIgnoreCase(nodeType))) {
            List<BaseNode> children = getChildren(node.getId());
            if (children != null && !children.isEmpty()) {
                for (BaseNode childNode : children) {
                    deleteNodesRecursively(childNode);
                }
            }
        }
        // This is just a workaround added for now.
        if (nodeType != null && NodeType.FOLDER.name().equalsIgnoreCase(nodeType)) {
            if (node.getGenericEntity() != null) {
                genericEntityRepository.delete(node.getGenericEntity());
            }

        } else if (nodeType != null && NodeType.ENTITY.name().equalsIgnoreCase(nodeType)) {
            genericEntityRepository.delete(node.getGenericEntity());
        }
        nodeRepository.delete(node);
    }

    @RequestMapping(value = "/api/nodes", method = RequestMethod.GET)
    public
    @ResponseBody
    List<BaseNode> findAll() {
        logger.debug("Finding all nodes");

        return nodeRepository.findAll();
    }

    @RequestMapping(value = "/api/nodes/{id}", method = RequestMethod.GET)
    public
    @ResponseBody
    BaseNode findById(@PathVariable("id") String id) {
        logger.debug("Finding node by id: " + id);

        BaseNode baseNode = nodeRepository.findOne(id);
        baseNode.getConversation();

        return baseNode;
    }

    @RequestMapping(value = "/api/nodes/{parentId}/children", method = RequestMethod.GET)
    public
    @ResponseBody
    List<BaseNode> getChildren(@PathVariable("parentId") String parentId) {
        logger.debug("Finding children nodes");

        return nodeRepository.getChildren(parentId);
    }

    @RequestMapping(value = "/api/nodes/{id}", method = RequestMethod.PUT, headers = "Accept=application/json")
    public
    @ResponseBody
    BaseNode update(@PathVariable("id") String id, @RequestBody NodeDTO updated) {
        logger.debug("Updating node with information: " + updated);

        BaseNode node = nodeRepository.findOne(updated.getId());

        if (updated.getName() != null) {
            node.setName(updated.getName());
        }

        if (updated.getDescription() != null) {
            node.setDescription(updated.getDescription());
        }

        if (updated.getStarred() != null) {
            node.setStarred(updated.getStarred());
        }

        nodeRepository.save(node);

        return node;
    }

    public TreeNode getProjectTree(String id) {
        return getProjectTree(id, null, null);
    }

    // Get tree-structure for a project. Id parameter is the project-reference node-id.
    @RequestMapping(value = "/api/nodes/{id}/tree", method = RequestMethod.GET)
    public
    @ResponseBody
    TreeNode getProjectTree(@PathVariable("id") String id,
                            @RequestParam(value = "search", required = false) String search,
                            @RequestParam(value = "sort", required = false) String sort) {
        // Note : There must be a better way of doing it. This method is written in a hurry.

        // Get project Id from the reference node
        BaseNode projectRefNode = nodeRepository.findOne(id);

        String projectId = projectRefNode.getProjectId();

        // Get the list of nodes for a project.
        List<BaseNode> listOfNodes = nodeRepository.searchNodesFromAProject(projectId, search != null ? search : "");

        // Creating a map of nodes with node-id as key
        Map<String, BaseNode> baseNodeMap = new HashMap<String, BaseNode>();
        Map<String, TreeNode> treeNodeMap = new HashMap<String, TreeNode>();
        TreeNode rootNode = null;
        TreeNode treeNode;
        TreeNode parentTreeNode;
        String methodType = "";

        for (BaseNode baseNode : listOfNodes) {
            String nodeId = baseNode.getId();
            baseNodeMap.put(nodeId, baseNode);

            if (baseNode.getConversation() != null) {
                methodType = baseNode.getConversation().getRfRequest().getMethodType();
            }
            treeNode = TreeNodeBuilder.createTreeNode(nodeId, baseNode.getName(), baseNode.getDescription(),
                baseNode.getWorkspaceId(), baseNode.getParentId(), baseNode.getPosition(), baseNode.getNodeType(),
                baseNode.getStarred(), methodType, baseNode.getLastModifiedDate(), baseNode.getLastModifiedBy());
            treeNode.setProjectId(projectId);
            treeNodeMap.put(nodeId, treeNode);
        }

        for (BaseNode baseNode : listOfNodes) {
            String nodeId = baseNode.getId();
            String parentId = baseNode.getParentId();

            treeNode = treeNodeMap.get(nodeId);

            if (NodeType.PROJECT.name().equals(baseNode.getNodeType())) {
                // Identify root node for a project
                rootNode = treeNode;
            } else {
                // Build parent node
                parentTreeNode = treeNodeMap.get(parentId);

                // Set parent tree node
                treeNode.setParent(parentTreeNode);

                // Add child node to the parent
                parentTreeNode.getChildren().add(treeNode);
            }
        }

        if (search != null && !search.trim().equals("")) {
            for (BaseNode baseNode : listOfNodes) {
                if (baseNode.getNodeType() != null && !NodeType.PROJECT.name().equals(baseNode.getNodeType())) {
                    TreeNode node = treeNodeMap.get(baseNode.getId());

                    if (node.getChildren().isEmpty()) {
                        TreeNode parent = treeNodeMap.get(baseNode.getParentId());
                        parent.getChildren().remove(node);
                    }
                }
            }
        }

        int order = 1;
        if (sort != null) {
            if (sort.trim().charAt(0) == '-') {
                order = -1;
                sort = sort.substring(1);
            }
            sortTree(rootNode, sort, order);
        } else {
            sortTree(rootNode, "position", order);
        }

        return rootNode;
    }

    private void sortTree(TreeNode rootNode, String sort, final int order) {
        if (rootNode != null && rootNode.getChildren() != null) {

            Comparator<TreeNode> comparator;

            switch (sort) {
                case "lastModified":
                    comparator = new Comparator<TreeNode>() {

                        @Override
                        public int compare(TreeNode o1, TreeNode o2) {
                            int val = 0;
                            if (o1.getLastModifiedDate() != null && o2.getLastModifiedDate() != null) {
                                val = o1.getLastModifiedDate().compareTo(o2.getLastModifiedDate());
                            } else if (o1.getLastModifiedDate() != null) {
                                val = 1;
                            } else if (o2.getLastModifiedDate() != null) {
                                val = -1;
                            }

                            return order * val;
                        }
                    };
                    break;
                case "name":
                    comparator = new Comparator<TreeNode>() {

                        @Override
                        public int compare(TreeNode o1, TreeNode o2) {

                            return order * o1.getName().compareTo(o2.getName());
                        }
                    };
                    break;

                default:
                    comparator = new Comparator<TreeNode>() {

                        @Override
                        public int compare(TreeNode o1, TreeNode o2) {

                            return order * o1.getPosition().compareTo(o2.getPosition());
                        }
                    };
                    break;
            }

            sortTreeNodes(rootNode, comparator);

        }
    }

    private void sortTreeNodes(TreeNode rootNode, Comparator<TreeNode> comparator) {
        if (rootNode != null && rootNode.getChildren() != null) {
            List<TreeNode> childs = rootNode.getChildren();
            for (TreeNode node : childs) {
                sortTreeNodes(node, comparator);
            }

            Collections.sort(childs, comparator);

            if (!childs.isEmpty()) {
                rootNode.setLastModifiedDate(childs.get(0).getLastModifiedDate());
            }
        }
    }

    @RequestMapping(value = "/api/workspaces/{workspaceId}/nodes/starred", method = RequestMethod.GET)
    public
    @ResponseBody
    List<NodeDTO> findStarredNodes(@PathVariable("workspaceId") String workspaceId,
                                   @RequestParam(value = "page", required = false) Integer page,
                                   @RequestParam(value = "limit", required = false) Integer limit,
                                   @RequestParam(value = "search", required = false) String search,
                                   @RequestParam(value = "sortBy", required = false) String sortBy) {
        logger.debug("Finding starred nodes.");

        int pageNo = 0;
        if (page != null && page > 0) {
            pageNo = page;
        }

        int numberOfRecords = 10;
        if (limit != null && limit > 0) {
            numberOfRecords = limit;
        }
        Sort sort = new Sort(Direction.DESC, "lastModifiedDate");
        if ("name".equals(sortBy)) {
            sort = new Sort(Direction.ASC, "name");
        } else if ("lastRun".equals(sortBy)) {
            sort = new Sort(Direction.DESC, "lastModifiedDate");
        } else if ("nameDesc".equals(sortBy)) {
            sort = new Sort(Direction.DESC, "name");
        }
        Pageable pageable = new PageRequest(pageNo, numberOfRecords, sort);

        Page<BaseNode> paginatedStarredNodes = nodeRepository.findStarredNodes(workspaceId,
            search != null ? search : "", pageable);

        List<BaseNode> starredNodes = paginatedStarredNodes.getContent();
        long totalElements = paginatedStarredNodes.getTotalElements();

        List<NodeDTO> response = new ArrayList<NodeDTO>();
        for (BaseNode item : starredNodes) {
            response.add(EntityToDTO.toDTO(item));
        }

        System.out.println("totalElements : " + totalElements);
        return response;
    }

    @RequestMapping(value = "/api/nodes/{id}/tags", method = RequestMethod.POST, headers = "Accept=application/json")
    public
    @ResponseBody
    Boolean addTags(@PathVariable("id") String id, @RequestBody List<TagDTO> tagDTOs) {
        logger.debug("Adding the following tags: " + tagDTOs);

        BaseNode node = nodeRepository.findOne(id);

        List<Tag> tags = new ArrayList<Tag>();
        if (tagDTOs != null && !tagDTOs.isEmpty()) {
            List<String> tagIds = new ArrayList<String>();
            for (TagDTO tagDTO : tagDTOs) {
                tagIds.add(tagDTO.getId());
            }
            tags = (List<Tag>)tagRepository.findAll(tagIds);
        }
        node.setTags(tags);

        nodeRepository.save(node);
        return Boolean.TRUE;
    }

    @RequestMapping(value = "/api/nodes/{id}/requests", method = RequestMethod.GET)
    public
    @ResponseBody
    List<BaseNode> getProjectRequests(@PathVariable("id") String id,
                                      @RequestParam(value = "page", required = false) Integer page,
                                      @RequestParam(value = "limit", required = false) Integer limit,
                                      @RequestParam(value = "search", required = false) String search,
                                      @RequestParam(value = "sort", required = false) String sortBy) {
        // Note : There must be a better way of doing it. This method is written in a hurry.

        // Get project Id from the reference node
        BaseNode projectRefNode = nodeRepository.findOne(id);

        String projectId = projectRefNode.getProjectId();

        List<BaseNode> requestsNodes = nodeRepository.findRequestsFromAProject(projectId, search != null ? search : "");

        return requestsNodes;
    }

    @RequestMapping(value = "/api/nodes/{id}/move", method = RequestMethod.POST, headers = "Accept=application/json")
    public
    @ResponseBody
    void move(@PathVariable("id") String id, @RequestParam(value = "newRefNodeId", required = true) String newRefNodeId,
              @RequestParam(value = "position", required = true) String position) {

        BaseNode node = nodeRepository.findOne(id);
        Long oldPosition = node.getPosition();
        BaseNode newRefNode = nodeRepository.findOne(newRefNodeId);
        BaseNode oldParentNode = nodeRepository.findOne(node.getParentId());

        Long newPosition;
        String newParentId;
        if (position.equals("over")) {
            newParentId = newRefNode.getId();
            newPosition = (long)1;
        } else if (position.equals("before")) {
            newParentId = newRefNode.getParentId();
            newPosition = newRefNode.getPosition();
        } else {
            newParentId = newRefNode.getParentId();
            newPosition = newRefNode.getPosition() + 1;
        }

        // Not allowed to save request under a request
        if (position.equals("over") && !(newRefNode.getNodeType().equalsIgnoreCase(PROJECT) || newRefNode.getNodeType()
            .equalsIgnoreCase("FOLDER"))) {
            return;
        }
        // Special case where -1 getting saved for non-project node
        if (!(node.getNodeType() != null && node.getNodeType().equalsIgnoreCase(PROJECT)) && newParentId.equals("-1")) {
            return;
        }
        // update new folder
        List<BaseNode> newFolderChildren;
        if (newRefNode.getNodeType() != null && (newRefNode.getNodeType().equalsIgnoreCase(PROJECT) || newRefNode
            .getNodeType().equalsIgnoreCase("FOLDER"))) {
            newFolderChildren = nodeRepository.getChildren(newRefNode.getId());
        } else {
            newFolderChildren = nodeRepository.getChildren(newRefNode.getParentId());
        }

        node.setParentId(newParentId);
        node.setPosition(newPosition);
        nodeRepository.save(node);
        for (BaseNode newFolderChild : newFolderChildren) {
            if (newFolderChild.getPosition() >= newPosition && !newFolderChild.getId().equals(id)) {
                newFolderChild.setPosition(newFolderChild.getPosition() + 1);
                nodeRepository.save(newFolderChild);
            }
        }

        //If node is moved within the same folder, updating new folder is sufficient
        if (oldParentNode.getId().equals(newParentId)) {
            return;
        }
        // update old folder
        List<BaseNode> oldFolderChildren = nodeRepository.getChildren(oldParentNode.getId());
        if (oldFolderChildren != null && !oldFolderChildren.isEmpty()) {
            for (BaseNode oldFolderChild : oldFolderChildren) {
                if (oldFolderChild.getPosition() >= oldPosition) {
                    oldFolderChild.setPosition(oldFolderChild.getPosition() - 1);
                    nodeRepository.save(oldFolderChild);
                }
            }
        }
    }

    @RequestMapping(value = "/api/workspaces/{workspaceId}/projects", method = RequestMethod.GET)
    public
    @ResponseBody
    List<BaseNode> findProjectsFromAWorkspace(@PathVariable("workspaceId") String workspaceId,
                                              @RequestParam(value = "search", required = false) String search) {
        return nodeRepository.findProjectsfromAWorkspace(workspaceId, search != null ? search : "");
    }
}
